package example

import kotlin.math.max
import kotlin.math.min
import kotlin.random.Random
import kotlin.time.Clock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Instant
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

data class FakeDataItem(
	val commonInfo: CommonInfo,
	val statInfo: StatInfo,
	val commandsInfo: List<CommandInfo>,
	val slowLogRecords: List<SlowLogRecord>,
	val rawKeyValuePairs: List<RawKeyValuePair>,
) {

	data class CommonInfo(
		val uptime: Duration,
		val processId: Int,
		val latency: Duration,
		val version: String,
		val role: String,
	)

	data class StatInfo(
		val time: Instant,
		val totalOperations: Long,
		val operationsPerSecond: Long,
		val sysCpuUtilizationPercent: Float,
		val userCpuUtilizationPercent: Float,
		val totalRxBytes: Long,
		val rxBytesPerSecond: Long,
		val totalTxBytes: Long,
		val txBytesPerSecond: Long,
		val maxMemoryBytes: Long,
		val memoryBytesUsage: Long,
		val fragmentationRatio: Float,
		val memoryRssBytes: Long,
		val hitRatePercent: Float,
		val totalKeys: Long,
		val keysToExpire: Long,
		val keysToExpirePerSecond: Long,
		val evictedKeysPerSecond: Long,
	)

	data class CommandInfo(
		val command: String,
		val calls: Long = 0L,
		val callsPercent: Float = 0.0f,
		val usec: Long = 0L,
		val usecPercent: Float = 0.0f,
		val usecPerCall: Long = 0L,
		val usecPerCallPercent: Float = 0.0f,
	)

	data class SlowLogRecord(
		val id: Long,
		val time: Instant,
		val execTime: Duration,
		val command: String,
		val clientIp: String,
		val clientName: String,
	)

	data class RawKeyValuePair(
		val key: String,
		val value: String,
	)
}

object FakeDataGenerator {
	val fakeDataItemFlow: Flow<FakeDataItem> = flow {
		var lastItem: FakeDataItem? = null
		while (true) {
			delay(1000L) // update once per second
			lastItem = generateFakeDataItem(lastItem).also { newItem -> emit(newItem) }
		}
	}

	private fun generateFakeDataItem(lastItem: FakeDataItem?): FakeDataItem {
		val newStatInfo = generateStatInfo(lastItem?.statInfo)
		return FakeDataItem(
			commonInfo = generateCommonInfo(
				lastItem?.commonInfo,
				newStatInfo.time - (lastItem?.statInfo?.time ?: Clock.System.now()),
			),
			statInfo = newStatInfo,
			commandsInfo = generateCommandsInfo(lastItem?.commandsInfo),
			slowLogRecords = generateSlowLogRecords(lastItem?.slowLogRecords),
			rawKeyValuePairs = generateRawKeyValuePairs(lastItem?.rawKeyValuePairs),
		)
	}

	private fun generateCommonInfo(
		lastCommonInfo: FakeDataItem.CommonInfo?,
		duration: Duration,
	): FakeDataItem.CommonInfo {
		return FakeDataItem.CommonInfo(
			uptime = (
				lastCommonInfo?.uptime
					?: Random.nextLong(0L, 1_000_000_000L).milliseconds
				) + duration,
			processId = lastCommonInfo?.processId ?: Random.nextInt(1, 100),
			latency = Random.nextInt(1, 70).milliseconds,
			version = lastCommonInfo?.version ?: listOf(
				Random.nextInt(0, 7),
				Random.nextInt(0, 10),
				Random.nextInt(0, 25),
			).joinToString("."),
			role = lastCommonInfo?.role ?: listOf("master", "slave", "sentinel", "cluster").random(),
		)
	}

	private fun generateStatInfo(lastStatInfo: FakeDataItem.StatInfo?): FakeDataItem.StatInfo {
		val operationsPerSecond = Random.nextLong(0L, 100_000L)
		val rxBytesPerSecond = Random.nextLong(0L, 5_000_000L)
		val txBytesPerSecond = Random.nextLong(0L, 5_000_000L)
		val keysToExpirePerSecond = Random.nextLong(0L, 100_000L)
		val evictedKeysPerSecond = Random.nextLong(0L, 100_000L)
		return FakeDataItem.StatInfo(
			time = Clock.System.now(),
			totalOperations = (lastStatInfo?.totalOperations ?: 0L) + operationsPerSecond,
			operationsPerSecond = operationsPerSecond,
			sysCpuUtilizationPercent = Random.nextFloat() * 100.0f,
			userCpuUtilizationPercent = Random.nextFloat() * 100.0f,
			totalRxBytes = (lastStatInfo?.totalRxBytes ?: 0L) + rxBytesPerSecond,
			rxBytesPerSecond = rxBytesPerSecond,
			totalTxBytes = (lastStatInfo?.totalTxBytes ?: 0L) + txBytesPerSecond,
			txBytesPerSecond = txBytesPerSecond,
			maxMemoryBytes = 15_000_000_000 + Random.nextLong(0L, 100_000_000L),
			memoryBytesUsage = Random.nextLong(0L, 500_000_000L),
			fragmentationRatio = (Random.nextFloat() + 0.4f) * (Random.nextFloat() + 0.1f),
			memoryRssBytes = Random.nextLong(0L, 500_000_000L),
			hitRatePercent = min(1.0f, Random.nextFloat() + 0.8f) * 100.0f,
			totalKeys = Random.nextLong(0L, 10_000_000L),
			keysToExpire = max(
				0L,
				(lastStatInfo?.keysToExpire ?: 0L) + keysToExpirePerSecond - evictedKeysPerSecond,
			),
			keysToExpirePerSecond = keysToExpirePerSecond,
			evictedKeysPerSecond = evictedKeysPerSecond,
		)
	}

	private fun generateCommandsInfo(
		lastCommandsInfo: List<FakeDataItem.CommandInfo>?,
	): List<FakeDataItem.CommandInfo> {
		val commandsInfo = lastCommandsInfo.orEmpty().toMutableList()
		repeat(Random.nextInt(0, 7)) {
			val updatedCommand = Commands[Random.nextInt(0, Commands.size)]
			val commandInfo = commandsInfo.find { it.command == updatedCommand }
				?.also { commandsInfo.remove(it) }
				?: FakeDataItem.CommandInfo(updatedCommand)
			commandsInfo.add(
				commandInfo.copy(
					calls = commandInfo.calls + Random.nextLong(0L, 1000L),
					usec = commandInfo.usec + Random.nextLong(0L, 2500L),
					usecPerCall = commandInfo.usecPerCall + Random.nextLong(0L, 200L),
				),
			)
		}
		val callsSum = commandsInfo.sumOf { it.calls }.toFloat()
		val usecSum = commandsInfo.sumOf { it.usec }.toFloat()
		val usecPerCallSum = commandsInfo.sumOf { it.usecPerCall }.toFloat()
		return commandsInfo
			.map {
				it.copy(
					callsPercent = (it.calls / callsSum) * 100.0f,
					usecPercent = (it.usec / usecSum) * 100.0f,
					usecPerCallPercent = (it.usecPerCall / usecPerCallSum) * 100.0f,
				)
			}
			.sortedBy { it.calls }
	}

	private fun generateSlowLogRecords(
		lastSlogLogRecords: List<FakeDataItem.SlowLogRecord>?,
	): List<FakeDataItem.SlowLogRecord> {
		var id = lastSlogLogRecords?.lastOrNull()?.id ?: 0L
		return List(Random.nextInt(1, 10)) {
			FakeDataItem.SlowLogRecord(
				id = ++id,
				time = Clock.System.now() + Random.nextInt(0, 500_000).microseconds,
				execTime = Random.nextInt(5, 50_000).microseconds,
				command = Commands[Random.nextInt(0, Commands.size)],
				clientIp = listOf(
					Random.nextInt(0, 255),
					Random.nextInt(0, 255),
					Random.nextInt(0, 255),
					Random.nextInt(0, 255),
				).joinToString("."),
				clientName = createRandomString(Random.nextInt(0, 12)),
			)
		}
	}

	private fun generateRawKeyValuePairs(
		lastRawKeyValuePairs: List<FakeDataItem.RawKeyValuePair>?,
	): List<FakeDataItem.RawKeyValuePair> {
		val rawKeyValuePairs =
			(lastRawKeyValuePairs ?: RawKeys.map { FakeDataItem.RawKeyValuePair(it, "") }).toMutableList()
		repeat(Random.nextInt(0, 20)) {
			val index = Random.nextInt(0, rawKeyValuePairs.size)
			rawKeyValuePairs[index] = rawKeyValuePairs[index].copy(
				value = when (Random.nextInt(0, 3)) {
					0 -> createRandomString(Random.nextInt(0, 15))
					1 -> Random.nextLong().toString()
					else -> Random.nextFloat().toString()
				},
			)
		}
		return rawKeyValuePairs
	}
}

private fun createRandomString(length: Int): String {
	val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9')
	return (1..length)
		.map { allowedChars.random() }
		.joinToString("")
}

// from https://master--redis-doc.netlify.app/commands/
private val Commands = listOf(
	"acl cat",
	"acl deluser",
	"acl dryrun",
	"acl genpass",
	"acl getuser",
	"acl list",
	"acl load",
	"acl log",
	"acl save",
	"acl setuser",
	"acl users",
	"acl whoami",
	"append",
	"asking",
	"auth",
	"bgrewriteaof",
	"bgsave",
	"bitcount",
	"bitfield",
	"bitfield_ro",
	"bitop",
	"bitpos",
	"blmove",
	"blmpop",
	"blpop",
	"brpop",
	"brpoplpush",
	"bzmpop",
	"bzpopmax",
	"bzpopmin",
	"command count",
	"command docs",
	"command getkeys",
	"command getkeysandflags",
	"command info",
	"command list",
	"command",
	"config get",
	"config resetstat",
	"config rewrite",
	"config set",
	"copy",
	"dbsize",
	"decr",
	"decrby",
	"del",
	"discard",
	"dump",
	"echo",
	"eval",
	"eval_ro",
	"evalsha",
	"evalsha_ro",
	"exec",
	"exists",
	"expire",
	"expireat",
	"expiretime",
	"failover",
	"fcall",
	"fcall_ro",
	"flushall",
	"flushdb",
	"geoadd",
	"geodist",
	"geohash",
	"geopos",
	"georadius",
	"georadius_ro",
	"georadiusbymember",
	"georadiusbymember_ro",
	"geosearch",
	"geosearchstore",
	"get",
	"getbit",
	"getdel",
	"getex",
	"getrange",
	"getset",
	"hdel",
	"hello",
	"hexists",
	"hget",
	"hgetall",
	"hincrby",
	"hincrbyfloat",
	"hkeys",
	"hlen",
	"hmget",
	"hmset",
	"hrandfield",
	"hscan",
	"hset",
	"hsetnx",
	"hstrlen",
	"hvals",
	"incr",
	"incrby",
	"incrbyfloat",
	"info",
	"keys",
	"lastsave",
	"latency doctor",
	"latency graph",
	"latency histogram",
	"latency history",
	"latency latest",
	"latency reset",
	"lcs",
	"lindex",
	"linsert",
	"llen",
	"lmove",
	"lmpop",
	"lolwut",
	"lpop",
	"lpos",
	"lpush",
	"lpushx",
	"lrange",
	"lrem",
	"lset",
	"ltrim",
	"memory doctor",
	"memory malloc-stats",
	"memory purge",
	"memory stats",
	"memory usage",
	"mget",
	"migrate",
	"module list",
	"module load",
	"module loadex",
	"module unload",
	"monitor",
	"move",
	"mset",
	"msetnx",
	"multi",
	"object encoding",
	"object freq",
	"object idletime",
	"object refcount",
	"persist",
	"pexpire",
	"pexpireat",
	"pexpiretime",
	"pfadd",
	"pfcount",
	"pfdebug",
	"pfmerge",
	"pfselftest",
	"ping",
	"psetex",
	"psubscribe",
	"psync",
	"pttl",
	"publish",
	"pubsub channels",
	"pubsub numpat",
	"pubsub numsub",
	"pubsub shardchannels",
	"pubsub shardnumsub",
	"punsubscribe",
	"quit",
	"randomkey",
	"readonly",
	"readwrite",
	"rename",
	"renamenx",
	"replconf",
	"replicaof",
	"reset",
	"restore",
	"restore-asking",
	"role",
	"rpop",
	"rpoplpush",
	"rpush",
	"rpushx",
	"sadd",
	"save",
	"scan",
	"scard",
	"sdiff",
	"sdiffstore",
	"select",
	"set",
	"setbit",
	"setex",
	"setnx",
	"setrange",
	"shutdown",
	"sinter",
	"sintercard",
	"sinterstore",
	"sismember",
	"slaveof",
	"slowlog get",
	"slowlog len",
	"slowlog reset",
	"smembers",
	"smismember",
	"smove",
	"sort",
	"sort_ro",
	"spop",
	"spublish",
	"srandmember",
	"srem",
	"sscan",
	"ssubscribe",
	"strlen",
	"subscribe",
	"substr",
	"sunion",
	"sunionstore",
	"sunsubscribe",
	"swapdb",
	"sync",
	"tfcall",
	"tfcallasync",
	"time",
	"touch",
	"ttl",
	"type",
	"unlink",
	"unsubscribe",
	"unwatch",
	"wait",
	"waitaof",
	"watch",
	"xack",
	"xadd",
	"xautoclaim",
	"xclaim",
	"xdel",
	"xlen",
	"xpending",
	"xrange",
	"xread",
	"xreadgroup",
	"xrevrange",
	"xsetid",
	"xtrim",
	"zadd",
	"zcard",
	"zcount",
	"zdiff",
	"zdiffstore",
	"zincrby",
	"zinter",
	"zintercard",
	"zinterstore",
	"zlexcount",
	"zmpop",
	"zmscore",
	"zpopmax",
	"zpopmin",
	"zrandmember",
	"zrange",
	"zrangebylex",
	"zrangebyscore",
	"zrangestore",
	"zrank",
	"zrem",
	"zremrangebylex",
	"zremrangebyrank",
	"zremrangebyscore",
	"zrevrange",
	"zrevrangebylex",
	"zrevrangebyscore",
	"zrevrank",
	"zscan",
	"zscore",
	"zunion",
	"zunionstore",
)

// from https://master--redis-doc.netlify.app/commands/info/
private val RawKeys = listOf(
	"acl_access_denied_auth",
	"acl_access_denied_channel",
	"acl_access_denied_cmd",
	"acl_access_denied_key",
	"active_defrag_hits",
	"active_defrag_key_hits",
	"active_defrag_key_misses",
	"active_defrag_misses",
	"active_defrag_running",
	"allocator_active",
	"allocator_allocated",
	"allocator_frag_bytes",
	"allocator_frag_ratio",
	"allocator_resident",
	"allocator_rss_bytes",
	"allocator_rss_ratio",
	"aof_base_size",
	"aof_buffer_length",
	"aof_current_rewrite_time_sec",
	"aof_current_size",
	"aof_delayed_fsync",
	"aof_enabled",
	"aof_last_bgrewrite_status",
	"aof_last_cow_size",
	"aof_last_rewrite_time_sec",
	"aof_last_write_status",
	"aof_pending_bio_fsync",
	"aof_pending_rewrite",
	"aof_rewrite_buffer_length",
	"aof_rewrite_in_progress",
	"aof_rewrite_scheduled",
	"aof_rewrites",
	"arch_bits",
	"async_loading",
	"atomicvar_api",
	"blocked_clients",
	"client_recent_max_input_buffer",
	"client_recent_max_output_buffer",
	"clients_in_timeout_table",
	"cluster_connections",
	"cluster_enabled",
	"config_file",
	"configured_hz",
	"connected_clients",
	"connected_slaves",
	"current_active_defrag_time",
	"current_cow_peak",
	"current_cow_size",
	"current_cow_size_age",
	"current_eviction_exceeded_time",
	"current_fork_perc",
	"current_save_keys_processed",
	"current_save_keys_total",
	"dump_payload_sanitizations",
	"eventloop_cmd_per_cycle_max",
	"eventloop_cycles",
	"eventloop_duration_aof_sum",
	"eventloop_duration_cmd_sum",
	"eventloop_duration_cron_sum",
	"eventloop_duration_max",
	"eventloop_duration_sum",
	"evicted_clients",
	"evicted_keys",
	"executable",
	"expire_cycle_cpu_milliseconds",
	"expired_keys",
	"expired_stale_perc",
	"expired_time_cap_reached_count",
	"gcc_version",
	"hz",
	"instantaneous_eventloop_cycles_per_sec",
	"instantaneous_eventloop_duration_usec",
	"instantaneous_input_kbps",
	"instantaneous_input_repl_kbps",
	"instantaneous_ops_per_sec",
	"instantaneous_output_kbps",
	"instantaneous_output_repl_kbps",
	"io_threaded_reads_processed",
	"io_threaded_writes_processed",
	"io_threads_active",
	"keyspace_hits",
	"keyspace_misses",
	"latest_fork_usec",
	"lazyfree_pending_objects",
	"lazyfreed_objects",
	"loading",
	"loading_eta_seconds",
	"loading_loaded_bytes",
	"loading_loaded_perc",
	"loading_rdb_used_mem",
	"loading_start_time",
	"loading_total_bytes",
	"lru_clock",
	"master_failover_state",
	"master_host",
	"master_last_io_seconds_ago",
	"master_link_down_since_seconds",
	"master_link_status",
	"master_port",
	"master_repl_offset",
	"master_replid",
	"master_replid2",
	"master_sync_in_progress",
	"master_sync_last_io_seconds_ago",
	"master_sync_left_bytes",
	"master_sync_perc",
	"master_sync_read_bytes",
	"master_sync_total_bytes",
	"maxclients",
	"maxmemory",
	"maxmemory_human",
	"maxmemory_policy",
	"mem_allocator",
	"mem_aof_buffer",
	"mem_clients_normal",
	"mem_clients_slaves",
	"mem_cluster_links",
	"mem_fragmentation_bytes",
	"mem_fragmentation_ratio",
	"mem_not_counted_for_evict",
	"mem_replication_backlog",
	"mem_total_replication_buffers",
	"migrate_cached_sockets",
	"min_slaves_good_slaves",
	"module_fork_in_progress",
	"module_fork_last_cow_size",
	"multiplexing_api",
	"os",
	"process_id",
	"process_supervised",
	"pubsub_channels",
	"pubsub_patterns",
	"pubsubshard_channels",
	"rdb_bgsave_in_progress",
	"rdb_changes_since_last_save",
	"rdb_current_bgsave_time_sec",
	"rdb_last_bgsave_status",
	"rdb_last_bgsave_time_sec",
	"rdb_last_cow_size",
	"rdb_last_load_keys_expired",
	"rdb_last_load_keys_loaded",
	"rdb_last_save_time",
	"rdb_saves",
	"redis_build_id",
	"redis_git_dirty",
	"redis_git_sha1",
	"redis_mode",
	"redis_version",
	"rejected_connections",
	"repl_backlog_active",
	"repl_backlog_first_byte_offset",
	"repl_backlog_histlen",
	"repl_backlog_size",
	"replica_announced",
	"role",
	"rss_overhead_bytes",
	"rss_overhead_ratio",
	"run_id",
	"second_repl_offset",
	"sentinel_masters",
	"sentinel_running_scripts",
	"sentinel_scripts_queue_length",
	"sentinel_simulate_failure_flags",
	"sentinel_tilt",
	"sentinel_tilt_since_seconds",
	"server_time_usec",
	"shutdown_in_milliseconds",
	"slave_expires_tracked_keys",
	"slave_priority",
	"slave_read_only",
	"slave_read_repl_offset",
	"slave_repl_offset",
	"stat_reply_buffer_expands",
	"stat_reply_buffer_shrinks",
	"sync_full",
	"sync_partial_err",
	"sync_partial_ok",
	"tcp_port",
	"total_active_defrag_time",
	"total_blocking_keys",
	"total_blocking_keys_on_nokey",
	"total_commands_processed",
	"total_connections_received",
	"total_error_replies",
	"total_eviction_exceeded_time",
	"total_forks",
	"total_net_input_bytes",
	"total_net_output_bytes",
	"total_net_repl_input_bytes",
	"total_net_repl_output_bytes",
	"total_reads_processed",
	"total_system_memory",
	"total_system_memory_human",
	"total_writes_processed",
	"tracking_clients",
	"tracking_total_items",
	"tracking_total_keys",
	"tracking_total_prefixes",
	"unexpected_error_replies",
	"uptime_in_days",
	"uptime_in_seconds",
	"used_cpu_sys",
	"used_cpu_sys_children",
	"used_cpu_sys_main_thread",
	"used_cpu_user",
	"used_cpu_user_children",
	"used_cpu_user_main_thread",
	"used_memory",
	"used_memory_dataset",
	"used_memory_dataset_perc",
	"used_memory_human",
	"used_memory_lua",
	"used_memory_lua_human",
	"used_memory_overhead",
	"used_memory_peak",
	"used_memory_peak_human",
	"used_memory_peak_perc",
	"used_memory_rss",
	"used_memory_rss_human",
	"used_memory_scripts",
	"used_memory_scripts_human",
	"used_memory_startup",
)
